# Copyright (c) 2021, yuxinyong and contributors
# For license information, please see license.txt

from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
import frappe
from frappe import _
from frappe.utils import getdate,cstr,ceil
from frappe.model.document import Document
from frappe.query_builder import Case, Criterion,Order
import json


whole_number_uom_list = frappe.get_all("UOM", filters={"must_be_whole_number":1}, pluck = "name")

class PlanOrder(Document):
    pass


@frappe.whitelist()
def create_work_order(plan_order, default_wip_warehouse = None, bulk_mode = False):
    if isinstance(plan_order, str):
        plan_order = frappe._dict(json.loads(plan_order))
    if (not plan_order) or (not plan_order.convertible) or plan_order.converted: return    
    if not default_wip_warehouse:
        default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
    bom_no = plan_order.bom_no or get_item_details(plan_order.item_code).get("bom_no")    
    if bom_no:
        wo_order = frappe.new_doc("Work Order")
        if plan_order.stock_uom in whole_number_uom_list:
            qty = ceil(plan_order.qty)
        else:
            qty = plan_order.qty
        wo_order.update({
            "production_item": plan_order.item_code,
            "qty": qty,
            "fg_warehouse": plan_order.warehouse,
            "wip_warehouse": default_wip_warehouse,
            "description": plan_order.item_name,
            "stock_uom": plan_order.stock_uom,
            "expected_delivery_date": plan_order.rescheduled_due_date or plan_order.due_date,
            "sales_order": plan_order.ref_doctype == "Sales Order" and plan_order.ref_docname or None,
            "sales_order_item":plan_order.ref_doctype == "Sales Order" and plan_order.ref_doc_item or None,
            "bom_no": bom_no,
            "planned_start_date": plan_order.rescheduled_order_release_date or plan_order.order_release_date,
            "company": plan_order.company
        })
        wo_order.set_work_order_operations()
        if bulk_mode:
            wo_order.save()
            wo_order.add_comment("Comment", _("Converted from plan order {0}".format(plan_order.name)))
            return wo_order.name
        else:        
            return wo_order.as_dict()

@frappe.whitelist()
def create_purchase_order(plan_order, bulk_mode = False):
    if isinstance(plan_order, str):
        plan_order = frappe._dict(json.loads(plan_order))
    if (not plan_order) or not plan_order.convertible or plan_order.converted: return
    
    po_order = frappe.new_doc("Purchase Order")
    po_order.update({
        "supplier": plan_order.supplier,
        "schedule_date": plan_order.rescheduled_due_date or plan_order.due_date,
        "is_subcontracted": "Yes" if plan_order.supply_method == "Subcontract" else "No",
        "company": plan_order.company
    })
    item_code_map = get_item_code_map([plan_order.item_code])
    item_info_dict = item_code_map.get(plan_order.item_code, frappe._dict())
    qty, stock_qty, uom = get_qty(plan_order.qty, item_info_dict)    
    po_order.append('items',{
        "item_code": plan_order.item_code,
        "item_name": plan_order.item_name,
		"description": item_info_dict.description,
        "qty": qty,
        "uom": uom,
		"stock_uom": plan_order.stock_uom
    })
    if bulk_mode:
        po_order.save()
        po_order.add_comment("Comment", _("Converted from plan order {0}".format(plan_order.name)))
    else:
        return po_order.as_dict()

@frappe.whitelist()
def create_material_request(plan_order, bulk_mode = False):
    if isinstance(plan_order, str):
        plan_order = frappe._dict(json.loads(plan_order))
    if (not plan_order) or (not plan_order.convertible) or plan_order.converted: return
    
    mr = frappe.new_doc("Material Request")
    mr.update({
        "material_request_type":plan_order.supply_method,
        "customer": plan_order.customer,
        "schedule_date": plan_order.rescheduled_due_date or plan_order.due_date,
        "company": plan_order.company
    })
    item_code_map = get_item_code_map([plan_order.item_code])
    item_info_dict = item_code_map.get(plan_order.item_code, frappe._dict())
    qty, stock_qty, uom = get_qty(plan_order.qty, item_info_dict)    
    mr.append('items',{
        "item_code": plan_order.item_code,
        "item_name": plan_order.item_name,
		"description": item_info_dict.description,
        "qty": qty,
        "uom": uom,
		"stock_uom": plan_order.stock_uom
    })
    if bulk_mode:
        mr.save()
        mr.add_comment("Comment", _("Converted from plan order {0}".format(plan_order.name)))
    else:
        return mr.as_dict()

@frappe.whitelist()
def bulk_convert(docnames = None, company = None, mrp = None, for_supply_method = None):
    """order_category: Work Order, Subcontract, Purchase Order, Material Request"""
    filters = {"convertible": 1, "converted": 0}
    if company:
        filters["company"] = company
    else:
        if isinstance(docnames, str):
            docnames = json.loads(docnames)
        filters["name"] = ("in", docnames)
    if mrp:
        filters["mrp"] = mrp
    plan_order_list = frappe.get_all("Plan Order",    filters = filters, 
        fields =["name","item_code", "company"])
    if not plan_order_list:
        return _("No plan orders can be converted")
    # 按供应商合并采购订单{供应类型(采购/委外):{供应商:doc}
    po_map, mr_map, wo_map = {}, {}, {}
    default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
    po_count, wo_count, mr_count, po_fail_count, wo_fail_count, mr_fail_count = [0]*6
    wo_fail_msg, mr_fail_msg, po_fail_msg = set(), set(), set()
    item_code_list = tuple({row.item_code for row in plan_order_list})
    item_code_map = get_item_code_map(item_code_list)
    docnames = [row.name for row in plan_order_list]
    for docname in docnames:
        try:
            plan_order = frappe.get_doc("Plan Order", docname)
            supplier, customer, supply_method, company, bom_no = (
                plan_order.supplier, plan_order.customer, plan_order.supply_method,
                plan_order.company, plan_order.bom_no)    
            if not supply_method: continue
            if supply_method == 'Purchase' and plan_order.is_subcontracted:
                supply_method == 'Subcontract'

            if for_supply_method and for_supply_method != supply_method: continue

            if supply_method in ("Purchase", "Subcontract") and supplier:
                if supply_method == "Subcontract" and not bom_no: continue
                item_date_plan_order_map = po_map.setdefault((supply_method, supplier, company), {})
                item_date_key = (plan_order.item_code, plan_order.rescheduled_due_date or plan_order.due_date)
                # combine qty for same item and due date
                if item_date_key in item_date_plan_order_map:
                    item_date_plan_order_map[item_date_key].qty += plan_order.qty
                    item_date_plan_order_map[item_date_key].docnames.append(plan_order.name)
                else:
                    item_date_plan_order_map[item_date_key] = plan_order
                    item_date_plan_order_map[item_date_key].docnames = [plan_order.name]  
            if supply_method in ("Customer Provided") and customer:
                item_date_plan_order_map = mr_map.setdefault((supply_method, customer, company), {})
                item_date_key = (plan_order.item_code, plan_order.rescheduled_due_date or plan_order.due_date)
                # combine qty for same item and due date
                if item_date_key in item_date_plan_order_map:
                    item_date_plan_order_map[item_date_key].qty += plan_order.qty
                    item_date_plan_order_map[item_date_key].docnames.append(plan_order.name)
                else:
                    item_date_plan_order_map[item_date_key] = plan_order
                    item_date_plan_order_map[item_date_key].docnames = [plan_order.name]                            
            elif supply_method == "Manufacture":                
                wo = create_work_order(plan_order, default_wip_warehouse = default_wip_warehouse, bulk_mode = True)
                plan_order.converted = 1
                plan_order.save()
                wo_count += 1
        except Exception as e:
            wo_fail_count += 1            
            wo_fail_msg.add(cstr(e))
            #raise
    for (supply_method, supplier, company), item_date_plan_order_map in po_map.items():
        try:
            po = frappe.new_doc("Purchase Order")
            po.update(frappe._dict({
                "supplier": supplier,
                "company": company,
                "is_subcontracted": "Yes" if supply_method == "Subcontract" else "No"
            }))
            docs = item_date_plan_order_map.values()
            #keep track of available over qty due to min order qty which can cover next item
            item_over_qty_map = frappe._dict()
            docnames = []
            for doc in docs:
                item_code = doc.item_code
                remaining_over_qty = item_over_qty_map.get(item_code, 0)
                if doc.qty > remaining_over_qty:
                    doc.qty -= remaining_over_qty
                    item_over_qty_map[item_code] = 0
                else:
                    doc.qty = 0
                    item_over_qty_map[item_code] -= doc.qty
                docnames.extend(doc.docnames)
                if doc.qty:
                    item_info_dict = item_code_map.get(doc.item_code, frappe._dict())
                    qty, stock_qty, uom = get_qty(doc.qty, item_info_dict)
                    po.append("items",{
                        "item_code": doc.item_code,
                        "item_name": doc.item_name,
                        "uom": uom,
                        "qty": qty,
                        "stock_uom": doc.stock_uom,
                        "schedule_date": doc.due_date if doc.due_date > getdate() else getdate()
                    })
                    item_over_qty_map[item_code] += (stock_qty - doc.qty)
            po = po.save()
            po_count += 1
            po.add_comment("Comment", _("Converted from plan orders {0}").format(",".join(docnames)))
            frappe.db.set_value("Plan Order", {"name":("in", docnames)}, "converted", 1)
        except Exception as e:
            po_fail_count += 1            
            po_fail_msg.add(docname + cstr(e))

    for (supply_method, customer, company), item_date_plan_order_map in mr_map.items():
        try:
            mr = frappe.new_doc("Material Request")
            mr.update(frappe._dict({
                "material_request_type": supply_method,
                "customer": customer,
                "company": company
            }))
            docs = item_date_plan_order_map.values()
            docnames = []
            for doc in docs:
                item_info_dict = item_code_map.get(doc.item_code, frappe._dict())
                qty, stock_qty, uom = get_qty(doc.qty, item_info_dict)
                mr.append("items",{
                    "item_code": doc.item_code,
                    "item_name": doc.item_name,
                    "uom": uom,
                    "qty": qty,
                    "stock_uom": doc.stock_uom,
                    "schedule_date": doc.due_date if doc.due_date > getdate() else getdate()
                })
                docnames.extend(doc.docnames)
            mr = mr.save()
            mr_count += 1
            mr.add_comment("Comment", _("Converted from plan orders {0}").format(",".join(docnames)))
            frappe.db.set_value("Plan Order", {"name":("in", docnames)}, "converted", 1)
        except Exception as e:
            mr_fail_count += 1            
            mr_fail_msg.add(cstr(e))            
            #raise
    msgs = []
    if po_count or po_fail_count:        
        msg = _("Converted to PO")
        msg += po_count and _("{0} OK").format(po_count) or ""
        msg += po_fail_msg and _("{0} Fail with error {1}").format(po_fail_count,'\n'.join(po_fail_msg)) or ""
        msgs.append(msg)
    if wo_count or wo_fail_count:        
        msg = _("Converted to Work Order")
        msg += wo_count and _("{0} OK").format(wo_count) or ""
        msg += wo_fail_count and _("{0} Fail with error {1}").format(wo_fail_count,'\n'.join(wo_fail_msg)) or ""
        msgs.append(msg)
    if mr_count or mr_fail_count:        
        msg = _("Converted to Material Request")
        msg += mr_count and _("{0} OK").format(mr_count) or ""
        msg += mr_fail_count and _("{0} Fail with error {1}").format(mr_fail_count,'\n'.join(mr_fail_msg)) or ""
        msgs.append(msg)
    if not msgs: msgs = [_("No convertible plan orders")]
    return '\n'.join(msgs)

def get_item_code_map(item_code_list):
    item = frappe.qb.DocType("Item")
    uom = frappe.qb.DocType("UOM Conversion Detail")
    item_list = frappe.qb.from_(
        item
    ).left_join(
        uom
    ).on(
        (item.name == uom.parent) & (uom.uom == item.purchase_uom)
    ).select(
        item.name,
		item.description,
        item.min_order_qty,
        item.min_pack_qty,
        item.stock_uom,
        item.purchase_uom,
        uom.conversion_factor
    ).where(
         Criterion.all([
            item.name.isin(item_code_list)
         ])
    ).run(as_dict = True)
    item_map = {item.name:frappe._dict({
                            'min_order_qty': item.min_order_qty,
                            'min_pack_qty': item.min_pack_qty,
                            'purchase_uom': item.purchase_uom,
							'stock_uom': item.stock_uom,
							'description': item.description,
                            'conversion_factor': item.conversion_factor
                            }) 
                for item in item_list}
    return item_map

def get_qty(qty = 0, item_info_dict = {}):
    """ get min pack based qty """
    if qty <=0:
        qty = 0
    if (item_info_dict.purchase_uom and 
        item_info_dict.purchase_uom != item_info_dict.stock_uom and 
        item_info_dict.conversion_factor):
        qty = qty / item_info_dict.conversion_factor
        uom = item_info_dict.purchase_uom
    else:
        uom = item_info_dict.stock_uom

    if uom in whole_number_uom_list:
        qty = ceil(qty)

    if qty > (item_info_dict.min_order_qty or 0) and item_info_dict.min_pack_qty:
        Quotient = qty / item_info_dict.min_pack_qty
        rounded_quotient = frappe.utils.floor(Quotient)
        if Quotient > rounded_quotient:
            qty = (rounded_quotient + 1) * item_info_dict.min_pack_qty
    elif qty < (item_info_dict.min_order_qty or 0):
        qty = item_info_dict.min_order_qty
    if (item_info_dict.purchase_uom and 
        item_info_dict.purchase_uom != item_info_dict.stock_uom and 
        item_info_dict.conversion_factor):
        stock_qty = qty * item_info_dict.conversion_factor
    else:
        stock_qty = qty    
    return (qty, stock_qty, uom)

"""
from zelin_am.agile_manufacturing.doctype.plan_order.plan_order import *
bulk_convert(company="frappedemo")
"""